/**
 * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mobi.Survey.charts;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;

import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.PathEffect;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.SparseArray;

import com.mobi.Survey.charts.XYMultipleSeriesRenderer.Orientation;

/**
 * The XY chart rendering class.
 */
public abstract class XYChart extends AbstractChart
{
	  /** The multiple series dataset. */
	  protected XYMultipleSeriesDataset mDataset;
	  /** The multiple series renderer. */
	  protected XYMultipleSeriesRenderer mRenderer;
	  /** The current scale value. */
	  private float mScale;
	  /** The current translate value. */
	  private float mTranslate;
	  /** The canvas center point. */
	  private Point mCenter;
	  /** The visible chart area, in screen coordinates. */
	  private Rect mScreenR;
	  /** The calculated range. */
	  private final SparseArray<double[]> mCalcRange = new SparseArray<double[]>();

	  /**
	   * The clickable areas for all points. The array index is the series index,
	   * and the RectF list index is the point index in that series.
	   */
	  private SparseArray<List<ClickableArea>> clickableAreas = new SparseArray<List<ClickableArea>>();

	  protected XYChart() {
	  }

	  /**
	   * Builds a new XY chart instance.
	   * 
	   * @param dataset the multiple series dataset
	   * @param renderer the multiple series renderer
	   */
	  public XYChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) {
	    mDataset = dataset;
	    mRenderer = renderer;
	  }

	  // TODO: javadoc
	  protected void setDatasetRenderer(XYMultipleSeriesDataset dataset,
	      XYMultipleSeriesRenderer renderer) {
	    mDataset = dataset;
	    mRenderer = renderer;
	  }

	  /**
	   * The graphical representation of the XY chart.
	   * 
	   * @param canvas the canvas to paint to
	   * @param x the top left x value of the view to draw to
	   * @param y the top left y value of the view to draw to
	   * @param width the width of the view to draw to
	   * @param height the height of the view to draw to
	   * @param paint the paint
	   */
	  public void draw(Canvas canvas, int x, int y, int width, int height, Paint paint) {
	    paint.setAntiAlias(mRenderer.isAntialiasing());
	    int legendSize = getLegendSize(mRenderer, height / 5, mRenderer.getAxisTitleTextSize());
	    int[] margins = mRenderer.getMargins();
	    int left = x + margins[1];
	    int top = y + margins[0];
	    int right = x + width - margins[3];
	    int sLength = mDataset.getSeriesCount();
	    String[] titles = new String[sLength];
	    for (int i = 0; i < sLength; i++) {
	      titles[i] = mDataset.getSeriesAt(i).getTitle();
	    }
	    if (mRenderer.isFitLegend() && mRenderer.isShowLegend()) {
	      legendSize = drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize,
	          paint, true);
	    }
	    int bottom = y + height - margins[2] - legendSize;
	    if (mScreenR == null) {
	      mScreenR = new Rect();
	    }
	    mScreenR.set(left, top, right, bottom);
	    drawBackground(mRenderer, canvas, x, y, width, height, paint, false, DefaultRenderer.NO_COLOR);

	    if (paint.getTypeface() == null
	        || !paint.getTypeface().toString().equals(mRenderer.getTextTypefaceName())
	        || paint.getTypeface().getStyle() != mRenderer.getTextTypefaceStyle()) {
	      paint.setTypeface(Typeface.create(mRenderer.getTextTypefaceName(),
	          mRenderer.getTextTypefaceStyle()));
	    }
	    Orientation or = mRenderer.getOrientation();
	    if (or == Orientation.VERTICAL) {
	      right -= legendSize;
	      bottom += legendSize - 20;
	    }
	    int angle = or.getAngle();
	    boolean rotate = angle == 90;
	    mScale = (float) (height) / width;
	    mTranslate = Math.abs(width - height) / 2;
	    if (mScale < 1) {
	      mTranslate *= -1;
	    }
	    mCenter = new Point((x + width) / 2, (y + height) / 2);
	    if (rotate) {
	      transform(canvas, angle, false);
	    }

	    int maxScaleNumber = -Integer.MAX_VALUE;
	    for (int i = 0; i < sLength; i++) {
	      maxScaleNumber = Math.max(maxScaleNumber, mDataset.getSeriesAt(i).getScaleNumber());
	    }
	    maxScaleNumber++;
	    if (maxScaleNumber < 0) {
	      return;
	    }
	    double[] minX = new double[maxScaleNumber];
	    double[] maxX = new double[maxScaleNumber];
	    double[] minY = new double[maxScaleNumber];
	    double[] maxY = new double[maxScaleNumber];
	    boolean[] isMinXSet = new boolean[maxScaleNumber];
	    boolean[] isMaxXSet = new boolean[maxScaleNumber];
	    boolean[] isMinYSet = new boolean[maxScaleNumber];
	    boolean[] isMaxYSet = new boolean[maxScaleNumber];

	    for (int i = 0; i < maxScaleNumber; i++) {
	      minX[i] = mRenderer.getXAxisMin(i);
	      maxX[i] = mRenderer.getXAxisMax(i);
	      minY[i] = mRenderer.getYAxisMin(i);
	      maxY[i] = mRenderer.getYAxisMax(i);
	      isMinXSet[i] = mRenderer.isMinXSet(i);
	      isMaxXSet[i] = mRenderer.isMaxXSet(i);
	      isMinYSet[i] = mRenderer.isMinYSet(i);
	      isMaxYSet[i] = mRenderer.isMaxYSet(i);
	      if (mCalcRange.get(i) == null) {
	        mCalcRange.put(i, new double[4]);
	      }
	    }
	    double[] xPixelsPerUnit = new double[maxScaleNumber];
	    double[] yPixelsPerUnit = new double[maxScaleNumber];
	    for (int i = 0; i < sLength; i++) {
	      XYSeries series = mDataset.getSeriesAt(i);
	      int scale = series.getScaleNumber();
	      if (series.getItemCount() == 0) {
	        continue;
	      }
	      if (!isMinXSet[scale]) {
	        double minimumX = series.getMinX();
	        minX[scale] = Math.min(minX[scale], minimumX);
	        mCalcRange.get(scale)[0] = minX[scale];
	      }
	      if (!isMaxXSet[scale]) {
	        double maximumX = series.getMaxX();
	        maxX[scale] = Math.max(maxX[scale], maximumX);
	        mCalcRange.get(scale)[1] = maxX[scale];
	      }
	      if (!isMinYSet[scale]) {
	        double minimumY = series.getMinY();
	        minY[scale] = Math.min(minY[scale], (float) minimumY);
	        mCalcRange.get(scale)[2] = minY[scale];
	      }
	      if (!isMaxYSet[scale]) {
	        double maximumY = series.getMaxY();
	        maxY[scale] = Math.max(maxY[scale], (float) maximumY);
	        mCalcRange.get(scale)[3] = maxY[scale];
	      }
	    }
	    for (int i = 0; i < maxScaleNumber; i++) {
	      if (maxX[i] - minX[i] != 0) {
	        xPixelsPerUnit[i] = (right - left) / (maxX[i] - minX[i]);
	      }
	      if (maxY[i] - minY[i] != 0) {
	        yPixelsPerUnit[i] = (float) ((bottom - top) / (maxY[i] - minY[i]));
	      }
	    }

	    boolean hasValues = false;
	    // use a linked list for these reasons:
	    // 1) Avoid a large contiguous memory allocation
	    // 2) We don't need random seeking, only sequential reading/writing, so
	    // linked list makes sense
	    clickableAreas = new SparseArray<List<ClickableArea>>();
	    for (int i = 0; i < sLength; i++) {
	      XYSeries series = mDataset.getSeriesAt(i);
	      int scale = series.getScaleNumber();
	      if (series.getItemCount() == 0) {
	        continue;
	      }

	      hasValues = true;
	      SimpleSeriesRenderer seriesRenderer = mRenderer.getSeriesRendererAt(i);

	      // int originalValuesLength = series.getItemCount();
	      // int valuesLength = originalValuesLength;
	      // int length = valuesLength * 2;

	      List<Float> points = new ArrayList<Float>();
	      List<Double> values = new ArrayList<Double>();
	      float yAxisValue = Math.min(bottom, (float) (bottom + yPixelsPerUnit[scale] * minY[scale]));
	      LinkedList<ClickableArea> clickableArea = new LinkedList<ClickableArea>();

	      clickableAreas.put(i, clickableArea);

	      SortedMap<Double, Double> range = series.getRange(minX[scale], maxX[scale], 1);
	      int startIndex = -1;

	      for (Entry<Double, Double> value : range.entrySet()) {

	        double xValue = value.getKey();
	        double yValue = value.getValue();
	        if (startIndex < 0) {
	          startIndex = series.getIndexForKey(xValue);
	        }

	        // points.add((float) (left + xPixelsPerUnit[scale]
	        // * (value.getKey().floatValue() - minX[scale])));
	        // points.add((float) (bottom - yPixelsPerUnit[scale]
	        // * (value.getValue().floatValue() - minY[scale])));
	        values.add(value.getKey());
	        values.add(value.getValue());

	        if (!isNullValue(yValue)) {
	          points.add((float) (left + xPixelsPerUnit[scale] * (xValue - minX[scale])));
	          points.add((float) (bottom - yPixelsPerUnit[scale] * (yValue - minY[scale])));
	        } else if (isRenderNullValues()) {
	          points.add((float) (left + xPixelsPerUnit[scale] * (xValue - minX[scale])));
	          points.add((float) (bottom - yPixelsPerUnit[scale] * (-minY[scale])));
	        } else {
	          if (points.size() > 0) {
	            drawSeries(series, canvas, paint, points, seriesRenderer, yAxisValue, i, or, startIndex);
	            ClickableArea[] clickableAreasForSubSeries = clickableAreasForPoints(
	                MathHelper.getFloats(points), MathHelper.getDoubles(values), yAxisValue, i,
	                startIndex);
	            clickableArea.addAll(Arrays.asList(clickableAreasForSubSeries));
	            points.clear();
	            values.clear();
	          }
	          clickableArea.add(null);
	        }
	      }

	      if (points.size() > 0) {
	        drawSeries(series, canvas, paint, points, seriesRenderer, yAxisValue, i, or, startIndex);
	        ClickableArea[] clickableAreasForSubSeries = clickableAreasForPoints(
	            MathHelper.getFloats(points), MathHelper.getDoubles(values), yAxisValue, i, startIndex);
	        clickableArea.addAll(Arrays.asList(clickableAreasForSubSeries));
	      }
	    }

	    // draw stuff over the margins such as data doesn't render on these areas
	    drawBackground(mRenderer, canvas, x, bottom, width, height - bottom, paint, true,
	        mRenderer.getMarginsColor());
	    drawBackground(mRenderer, canvas, x, y, width, margins[0], paint, true,
	        mRenderer.getMarginsColor());
	    if (or == Orientation.HORIZONTAL) {
	      drawBackground(mRenderer, canvas, x, y, left - x, height - y, paint, true,
	          mRenderer.getMarginsColor());
	      drawBackground(mRenderer, canvas, right, y, margins[3], height - y, paint, true,
	          mRenderer.getMarginsColor());
	    } else if (or == Orientation.VERTICAL) {
	      drawBackground(mRenderer, canvas, right, y, width - right, height - y, paint, true,
	          mRenderer.getMarginsColor());
	      drawBackground(mRenderer, canvas, x, y, left - x, height - y, paint, true,
	          mRenderer.getMarginsColor());
	    }

	    boolean showLabels = mRenderer.isShowLabels() && hasValues;
	    boolean showGridX = mRenderer.isShowGridX();
	    boolean showCustomTextGrid = mRenderer.isShowCustomTextGrid();
	    if (showLabels || showGridX) {
	      List<Double> xLabels = getValidLabels(getXLabels(minX[0], maxX[0], mRenderer.getXLabels()));
	      SparseArray<List<Double>> allYLabels = getYLabels(minY, maxY, maxScaleNumber);

	      int xLabelsLeft = left;
	      if (showLabels) {
	        paint.setColor(mRenderer.getXLabelsColor());
	        paint.setTextSize(mRenderer.getLabelsTextSize());
	        paint.setTextAlign(mRenderer.getXLabelsAlign());
	        if (mRenderer.getXLabelsAlign() == Align.LEFT) {
	          xLabelsLeft += mRenderer.getLabelsTextSize() / 4;
	        }
	      }
	      drawXLabels(xLabels, mRenderer.getXTextLabelLocations(), canvas, paint, xLabelsLeft, top,
	          bottom, xPixelsPerUnit[0], minX[0], maxX[0]);
	      drawYLabels(allYLabels, canvas, paint, maxScaleNumber, left, right, bottom, yPixelsPerUnit,
	          minY);

	      if (showLabels) {
	        paint.setColor(mRenderer.getLabelsColor());
	        for (int i = 0; i < maxScaleNumber; i++) {
	          Align axisAlign = mRenderer.getYAxisAlign(i);
	          Double[] yTextLabelLocations = mRenderer.getYTextLabelLocations(i);
	          for (Double location : yTextLabelLocations) {
	            if (minY[i] <= location && location <= maxY[i]) {
	              float yLabel = (float) (bottom - yPixelsPerUnit[i]
	                  * (location.doubleValue() - minY[i]));
	              String label = mRenderer.getYTextLabel(location, i);
	              paint.setColor(mRenderer.getYLabelsColor(i));
	              paint.setTextAlign(mRenderer.getYLabelsAlign(i));
	              if (or == Orientation.HORIZONTAL) {
	                if (axisAlign == Align.LEFT) {
	                  canvas.drawLine(left + getLabelLinePos(axisAlign), yLabel, left, yLabel, paint);
	                  drawText(canvas, label, left, yLabel - 2, paint, mRenderer.getYLabelsAngle());
	                } else {
	                  canvas.drawLine(right, yLabel, right + getLabelLinePos(axisAlign), yLabel, paint);
	                  drawText(canvas, label, right, yLabel - 2, paint, mRenderer.getYLabelsAngle());
	                }

	                if (showCustomTextGrid) {
	                  paint.setColor(mRenderer.getGridColor());
	                  canvas.drawLine(left, yLabel, right, yLabel, paint);
	                }
	              } else {
	                canvas.drawLine(right - getLabelLinePos(axisAlign), yLabel, right, yLabel, paint);
	                drawText(canvas, label, right + 10, yLabel - 2, paint, mRenderer.getYLabelsAngle());
	                if (showCustomTextGrid) {
	                  paint.setColor(mRenderer.getGridColor());
	                  canvas.drawLine(right, yLabel, left, yLabel, paint);
	                }
	              }
	            }
	          }
	        }
	      }

	      if (showLabels) {
	        paint.setColor(mRenderer.getLabelsColor());
	        float size = mRenderer.getAxisTitleTextSize();
	        paint.setTextSize(size);
	        paint.setTextAlign(Align.CENTER);
	        if (or == Orientation.HORIZONTAL) {
	          drawText(canvas, mRenderer.getXTitle(), x + width / 2,
	              bottom + mRenderer.getLabelsTextSize() * 4 / 3 + size, paint, 0);
	          for (int i = 0; i < maxScaleNumber; i++) {
	            Align axisAlign = mRenderer.getYAxisAlign(i);
	            if (axisAlign == Align.LEFT) {
	              drawText(canvas, mRenderer.getYTitle(i), x + size, y + height / 2, paint, -90);
	            } else {
	              drawText(canvas, mRenderer.getYTitle(i), x + width, y + height / 2, paint, -90);
	            }
	          }
	          paint.setTextSize(mRenderer.getChartTitleTextSize());
	          drawText(canvas, mRenderer.getChartTitle(), x + width / 2,
	              y + mRenderer.getChartTitleTextSize(), paint, 0);
	        } else if (or == Orientation.VERTICAL) {
	          drawText(canvas, mRenderer.getXTitle(), x + width / 2, y + height - size, paint, -90);
	          drawText(canvas, mRenderer.getYTitle(), right + 20, y + height / 2, paint, 0);
	          paint.setTextSize(mRenderer.getChartTitleTextSize());
	          drawText(canvas, mRenderer.getChartTitle(), x + size, top + height / 2, paint, 0);
	        }
	      }
	    }
	    if (or == Orientation.HORIZONTAL) {
	      drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, false);
	    } else if (or == Orientation.VERTICAL) {
	      transform(canvas, angle, true);
	      drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, false);
	      transform(canvas, angle, false);
	    }
	    if (mRenderer.isShowAxes()) {
	      paint.setColor(mRenderer.getAxesColor());
	      canvas.drawLine(left, bottom, right, bottom, paint);
	      boolean rightAxis = false;
	      for (int i = 0; i < maxScaleNumber && !rightAxis; i++) {
	        rightAxis = mRenderer.getYAxisAlign(i) == Align.RIGHT;
	      }
	      if (or == Orientation.HORIZONTAL) {
	        canvas.drawLine(left, top, left, bottom, paint);
	        if (rightAxis) {
	          canvas.drawLine(right, top, right, bottom, paint);
	        }
	      } else if (or == Orientation.VERTICAL) {
	        canvas.drawLine(right, top, right, bottom, paint);
	      }
	    }
	    if (rotate) {
	      transform(canvas, angle, true);
	    }
	  }

	  protected List<Double> getXLabels(double min, double max, int count) {
	    return MathHelper.getLabels(min, max, count);
	  }

	  protected SparseArray<List<Double>> getYLabels(double[] minY, double[] maxY, int maxScaleNumber) {
		  SparseArray<List<Double>> allYLabels = new SparseArray<List<Double>>();
	    for (int i = 0; i < maxScaleNumber; i++) {
	      allYLabels.put(i,
	          getValidLabels(MathHelper.getLabels(minY[i], maxY[i], mRenderer.getYLabels())));
	    }
	    return allYLabels;
	  }

	  protected Rect getScreenR() {
	    return mScreenR;
	  }

	  protected void setScreenR(Rect screenR) {
	    mScreenR = screenR;
	  }

	  private List<Double> getValidLabels(List<Double> labels) {
	    List<Double> result = new ArrayList<Double>(labels);
	    for (Double label : labels) {
	      if (label.isNaN()) {
	        result.remove(label);
	      }
	    }
	    return result;
	  }

	  /**
	   * Draws the series.
	   * 
	   * @param series the series
	   * @param canvas the canvas
	   * @param paint the paint object
	   * @param pointsList the points to be rendered
	   * @param seriesRenderer the series renderer
	   * @param yAxisValue the y axis value in pixels
	   * @param seriesIndex the series index
	   * @param or the orientation
	   * @param startIndex the start index of the rendering points
	   */
	  protected void drawSeries(XYSeries series, Canvas canvas, Paint paint, List<Float> pointsList,
	      SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, Orientation or,
	      int startIndex) {
	    BasicStroke stroke = seriesRenderer.getStroke();
	    Cap cap = paint.getStrokeCap();
	    Join join = paint.getStrokeJoin();
	    float miter = paint.getStrokeMiter();
	    PathEffect pathEffect = paint.getPathEffect();
	    Style style = paint.getStyle();
	    if (stroke != null) {
	      PathEffect effect = null;
	      if (stroke.getIntervals() != null) {
	        effect = new DashPathEffect(stroke.getIntervals(), stroke.getPhase());
	      }
	      setStroke(stroke.getCap(), stroke.getJoin(), stroke.getMiter(), Style.FILL_AND_STROKE,
	          effect, paint);
	    }
	    float[] points = MathHelper.getFloats(pointsList);
	    drawSeries(canvas, paint, points, seriesRenderer, yAxisValue, seriesIndex, startIndex);
	    
	    paint.setTextSize(seriesRenderer.getChartValuesTextSize());
	    if (or == Orientation.HORIZONTAL) {
	      paint.setTextAlign(Align.CENTER);
	    } else {
	      paint.setTextAlign(Align.LEFT);
	    }
	    if (seriesRenderer.isDisplayChartValues()) {
	      paint.setTextAlign(seriesRenderer.getChartValuesTextAlign());
	      drawChartValuesText(canvas, series, seriesRenderer, paint, points, seriesIndex, startIndex);
	    }
	    if (stroke != null) {
	      setStroke(cap, join, miter, style, pathEffect, paint);
	    }
	  }

	  private void setStroke(Cap cap, Join join, float miter, Style style, PathEffect pathEffect,
	      Paint paint) {
	    paint.setStrokeCap(cap);
	    paint.setStrokeJoin(join);
	    paint.setStrokeMiter(miter);
	    paint.setPathEffect(pathEffect);
	    paint.setStyle(style);
	  }

	  /**
	   * The graphical representation of the series values as text.
	   * 
	   * @param canvas the canvas to paint to
	   * @param series the series to be painted
	   * @param renderer the series renderer
	   * @param paint the paint to be used for drawing
	   * @param points the array of points to be used for drawing the series
	   * @param seriesIndex the index of the series currently being drawn
	   * @param startIndex the start index of the rendering points
	   */
	  protected void drawChartValuesText(Canvas canvas, XYSeries series, SimpleSeriesRenderer renderer,
	      Paint paint, float[] points, int seriesIndex, int startIndex) {
	    if (points.length > 1) { // there are more than one point
	      // record the first point's position
	      float previousPointX = points[0];
	      float previousPointY = points[1];
	      for (int k = 0; k < points.length; k += 2) {
	        if (k == 2) { // decide whether to display first two points' values or
	                      // not
	          if (Math.abs(points[2] - points[0]) > renderer.getDisplayChartValuesDistance() || Math.abs(points[3] - points[1]) > renderer.getDisplayChartValuesDistance()) {
	            // first point
	            drawText(canvas, getLabel(series.getY(startIndex)), points[0],
	                points[1] - renderer.getChartValuesSpacing(), paint, 0);
	            // second point
	            drawText(canvas, getLabel(series.getY(startIndex + 1)), points[2],
	                points[3] - renderer.getChartValuesSpacing(), paint, 0);

	            previousPointX = points[2];
	            previousPointY = points[3];
	          }
	        } else if (k > 2) {
	          // compare current point's position with the previous point's, if they
	          // are not too close, display
	          if (Math.abs(points[k] - previousPointX) > renderer.getDisplayChartValuesDistance()
	              || Math.abs(points[k + 1] - previousPointY) > renderer.getDisplayChartValuesDistance()) {
	            drawText(canvas, getLabel(series.getY(startIndex + k / 2)), points[k], points[k + 1]
	                - renderer.getChartValuesSpacing(), paint, 0);
	            previousPointX = points[k];
	            previousPointY = points[k + 1];
	          }
	        }
	      }
	    } else { // if only one point, display it
	      for (int k = 0; k < points.length; k += 2) {
	        drawText(canvas, getLabel(series.getY(startIndex + k / 2)), points[k], points[k + 1]
	            - renderer.getChartValuesSpacing(), paint, 0);
	      }
	    }
	  }

	  /**
	   * The graphical representation of a text, to handle both HORIZONTAL and
	   * VERTICAL orientations and extra rotation angles.
	   * 
	   * @param canvas the canvas to paint to
	   * @param text the text to be rendered
	   * @param x the X axis location of the text
	   * @param y the Y axis location of the text
	   * @param paint the paint to be used for drawing
	   * @param extraAngle the text angle
	   */
	  protected void drawText(Canvas canvas, String text, float x, float y, Paint paint,
	      float extraAngle) {
	    float angle = -mRenderer.getOrientation().getAngle() + extraAngle;
	    if (angle != 0) {
	      // canvas.scale(1 / mScale, mScale);
	      canvas.rotate(angle, x, y);
	    }
	    drawString(canvas, text, x, y, paint);
	    if (angle != 0) {
	      canvas.rotate(-angle, x, y);
	      // canvas.scale(mScale, 1 / mScale);
	    }
	  }

	  /**
	   * Transform the canvas such as it can handle both HORIZONTAL and VERTICAL
	   * orientations.
	   * 
	   * @param canvas the canvas to paint to
	   * @param angle the angle of rotation
	   * @param inverse if the inverse transform needs to be applied
	   */
	  private void transform(Canvas canvas, float angle, boolean inverse) {
	    if (inverse) {
	      canvas.scale(1 / mScale, mScale);
	      canvas.translate(mTranslate, -mTranslate);
	      canvas.rotate(-angle, mCenter.getX(), mCenter.getY());
	    } else {
	      canvas.rotate(angle, mCenter.getX(), mCenter.getY());
	      canvas.translate(-mTranslate, mTranslate);
	      canvas.scale(mScale, 1 / mScale);
	    }
	  }

	  /**
	   * The graphical representation of the labels on the X axis.
	   * 
	   * @param xLabels the X labels values
	   * @param xTextLabelLocations the X text label locations
	   * @param canvas the canvas to paint to
	   * @param paint the paint to be used for drawing
	   * @param left the left value of the labels area
	   * @param top the top value of the labels area
	   * @param bottom the bottom value of the labels area
	   * @param xPixelsPerUnit the amount of pixels per one unit in the chart labels
	   * @param minX the minimum value on the X axis in the chart
	   * @param maxX the maximum value on the X axis in the chart
	   */
	  protected void drawXLabels(List<Double> xLabels, Double[] xTextLabelLocations, Canvas canvas,
	      Paint paint, int left, int top, int bottom, double xPixelsPerUnit, double minX, double maxX) {
	    int length = xLabels.size();
	    boolean showLabels = mRenderer.isShowLabels();
	    boolean showGridY = mRenderer.isShowGridY();
	    for (int i = 0; i < length; i++) {
	      double label = xLabels.get(i);
	      float xLabel = (float) (left + xPixelsPerUnit * (label - minX));
	      if (showLabels) {
	        paint.setColor(mRenderer.getXLabelsColor());
	        canvas.drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint);
	        drawText(canvas, getLabel(label), xLabel, bottom + mRenderer.getLabelsTextSize() * 4 / 3,
	            paint, mRenderer.getXLabelsAngle());
	      }
	      if (showGridY) {
	        paint.setColor(mRenderer.getGridColor());
	        canvas.drawLine(xLabel, bottom, xLabel, top, paint);
	      }
	    }
	    drawXTextLabels(xTextLabelLocations, canvas, paint, showLabels, left, top, bottom,
	        xPixelsPerUnit, minX, maxX);
	  }

	  /**
	   * The graphical representation of the labels on the X axis.
	   * 
	   * @param allYLabels the Y labels values
	   * @param canvas the canvas to paint to
	   * @param paint the paint to be used for drawing
	   * @param maxScaleNumber the maximum scale number
	   * @param left the left value of the labels area
	   * @param right the right value of the labels area
	   * @param bottom the bottom value of the labels area
	   * @param yPixelsPerUnit the amount of pixels per one unit in the chart labels
	   * @param minY the minimum value on the Y axis in the chart
	   */
	  protected void drawYLabels(SparseArray<List<Double>> allYLabels, Canvas canvas, Paint paint,
	      int maxScaleNumber, int left, int right, int bottom, double[] yPixelsPerUnit, double[] minY) {
	    Orientation or = mRenderer.getOrientation();
	    boolean showGridX = mRenderer.isShowGridX();
	    boolean showLabels = mRenderer.isShowLabels();
	    for (int i = 0; i < maxScaleNumber; i++) {
	      paint.setTextAlign(mRenderer.getYLabelsAlign(i));
	      List<Double> yLabels = allYLabels.get(i);
	      int length = yLabels.size();
	      for (int j = 0; j < length; j++) {
	        double label = yLabels.get(j);
	        Align axisAlign = mRenderer.getYAxisAlign(i);
	        boolean textLabel = mRenderer.getYTextLabel(label, i) != null;
	        float yLabel = (float) (bottom - yPixelsPerUnit[i] * (label - minY[i]));
	        if (or == Orientation.HORIZONTAL) {
	          if (showLabels && !textLabel) {
	            paint.setColor(mRenderer.getYLabelsColor(i));
	            if (axisAlign == Align.LEFT) {
	              canvas.drawLine(left + getLabelLinePos(axisAlign), yLabel, left, yLabel, paint);
	              drawText(canvas, getLabel(label), left, yLabel - 2, paint,
	                  mRenderer.getYLabelsAngle());
	            } else {
	              canvas.drawLine(right, yLabel, right + getLabelLinePos(axisAlign), yLabel, paint);
	              drawText(canvas, getLabel(label), right, yLabel - 2, paint,
	                  mRenderer.getYLabelsAngle());
	            }
	          }
	          if (showGridX) {
	            paint.setColor(mRenderer.getGridColor());
	            canvas.drawLine(left, yLabel, right, yLabel, paint);
	          }
	        } else if (or == Orientation.VERTICAL) {
	          if (showLabels && !textLabel) {
	            paint.setColor(mRenderer.getYLabelsColor(i));
	            canvas.drawLine(right - getLabelLinePos(axisAlign), yLabel, right, yLabel, paint);
	            drawText(canvas, getLabel(label), right + 10, yLabel - 2, paint,
	                mRenderer.getYLabelsAngle());
	          }
	          if (showGridX) {
	            paint.setColor(mRenderer.getGridColor());
	            canvas.drawLine(right, yLabel, left, yLabel, paint);
	          }
	        }
	      }
	    }
	  }

	  /**
	   * The graphical representation of the text labels on the X axis.
	   * 
	   * @param xTextLabelLocations the X text label locations
	   * @param canvas the canvas to paint to
	   * @param paint the paint to be used for drawing
	   * @param left the left value of the labels area
	   * @param top the top value of the labels area
	   * @param bottom the bottom value of the labels area
	   * @param xPixelsPerUnit the amount of pixels per one unit in the chart labels
	   * @param minX the minimum value on the X axis in the chart
	   * @param maxX the maximum value on the X axis in the chart
	   */
	  protected void drawXTextLabels(Double[] xTextLabelLocations, Canvas canvas, Paint paint,
	      boolean showLabels, int left, int top, int bottom, double xPixelsPerUnit, double minX,
	      double maxX) {
	    boolean showCustomTextGrid = mRenderer.isShowCustomTextGrid();
	    if (showLabels) {
	      paint.setColor(mRenderer.getXLabelsColor());
	      for (Double location : xTextLabelLocations) {
	        if (minX <= location && location <= maxX) {
	          float xLabel = (float) (left + xPixelsPerUnit * (location.doubleValue() - minX));
	          paint.setColor(mRenderer.getXLabelsColor());
	          canvas
	              .drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint);
	          drawText(canvas, mRenderer.getXTextLabel(location), xLabel,
	              bottom + mRenderer.getLabelsTextSize() * 4 / 3, paint, mRenderer.getXLabelsAngle());
	          if (showCustomTextGrid) {
	            paint.setColor(mRenderer.getGridColor());
	            canvas.drawLine(xLabel, bottom, xLabel, top, paint);
	          }
	        }
	      }
	    }
	  }

	  // TODO: docs
	  public XYMultipleSeriesRenderer getRenderer() {
	    return mRenderer;
	  }

	  public XYMultipleSeriesDataset getDataset() {
	    return mDataset;
	  }

	  public double[] getCalcRange(int scale) {
	    return mCalcRange.get(scale);
	  }

	  public void setCalcRange(double[] range, int scale) {
	    mCalcRange.put(scale, range);
	  }

	  public double[] toRealPoint(float screenX, float screenY) {
	    return toRealPoint(screenX, screenY, 0);
	  }

	  public double[] toScreenPoint(double[] realPoint) {
	    return toScreenPoint(realPoint, 0);
	  }

	  private int getLabelLinePos(Align align) {
	    int pos = 4;
	    if (align == Align.LEFT) {
	      pos = -pos;
	    }
	    return pos;
	  }

	  /**
	   * Transforms a screen point to a real coordinates point.
	   * 
	   * @param screenX the screen x axis value
	   * @param screenY the screen y axis value
	   * @return the real coordinates point
	   */
	  public double[] toRealPoint(float screenX, float screenY, int scale) {
	    double realMinX = mRenderer.getXAxisMin(scale);
	    double realMaxX = mRenderer.getXAxisMax(scale);
	    double realMinY = mRenderer.getYAxisMin(scale);
	    double realMaxY = mRenderer.getYAxisMax(scale);
	    return new double[] {
	        (screenX - mScreenR.left) * (realMaxX - realMinX) / mScreenR.width() + realMinX,
	        (mScreenR.top + mScreenR.height() - screenY) * (realMaxY - realMinY) / mScreenR.height()
	            + realMinY };
	  }

	  public double[] toScreenPoint(double[] realPoint, int scale) {
	    double realMinX = mRenderer.getXAxisMin(scale);
	    double realMaxX = mRenderer.getXAxisMax(scale);
	    double realMinY = mRenderer.getYAxisMin(scale);
	    double realMaxY = mRenderer.getYAxisMax(scale);
	    if (!mRenderer.isMinXSet(scale) || !mRenderer.isMaxXSet(scale) || !mRenderer.isMinXSet(scale)
	        || !mRenderer.isMaxYSet(scale)) {
	      double[] calcRange = getCalcRange(scale);
	      realMinX = calcRange[0];
	      realMaxX = calcRange[1];
	      realMinY = calcRange[2];
	      realMaxY = calcRange[3];
	    }
	    return new double[] {
	        (realPoint[0] - realMinX) * mScreenR.width() / (realMaxX - realMinX) + mScreenR.left,
	        (realMaxY - realPoint[1]) * mScreenR.height() / (realMaxY - realMinY) + mScreenR.top };
	  }

 

	  /**
	   * The graphical representation of a series.
	   * 
	   * @param canvas the canvas to paint to
	   * @param paint the paint to be used for drawing
	   * @param points the array of points to be used for drawing the series
	   * @param seriesRenderer the series renderer
	   * @param yAxisValue the minimum value of the y axis
	   * @param seriesIndex the index of the series currently being drawn
	   * @param startIndex the start index of the rendering points
	   */
	  public abstract void drawSeries(Canvas canvas, Paint paint, float[] points,
	      SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex);

	  /**
	   * Returns the clickable areas for all passed points
	   * 
	   * @param points the array of points
	   * @param values the array of values of each point
	   * @param yAxisValue the minimum value of the y axis
	   * @param seriesIndex the index of the series to which the points belong
	   * @return an array of rectangles with the clickable area
	   * @param startIndex the start index of the rendering points
	   */
	  protected abstract ClickableArea[] clickableAreasForPoints(float[] points, double[] values,
	      float yAxisValue, int seriesIndex, int startIndex);

	  /**
	   * Returns if the chart should display the null values.
	   * 
	   * @return if null values should be rendered
	   */
	  protected boolean isRenderNullValues() {
	    return false;
	  }

	  /**
	   * Returns if the chart should display the points as a certain shape.
	   * 
	   * @param renderer the series renderer
	   */
	  public boolean isRenderPoints(SimpleSeriesRenderer renderer) {
	    return false;
	  }

	  /**
	   * Returns the default axis minimum.
	   * 
	   * @return the default axis minimum
	   */
	  public double getDefaultMinimum() {
	    return MathHelper.NULL_VALUE;
	  }

	  

	  /**
	   * Returns the chart type identifier.
	   * 
	   * @return the chart type
	   */
	  public abstract String getChartType();

}
